/*
 * map_handler.c
 *
 * This file contains the source code for the utility functions related to the
 * map structs.
 */

#include <stdio.h>
#include "map_handler.h"

#define SMALLCACHESIZE 32
#define LARGECACHESIZE 128
#define MAPBUFFERSIZE 512

/* TODO: The small cache can just be overlaid on top of the large cache,
 * eliminating the need of manually filling the small cache every time it
 * moves. */
/* TODO: Error handling. */
static FILE *current_map;
static unsigned int map_height;
static unsigned int map_width;
static unsigned int map_data_offset;
static unsigned int small_cache_x_offset;
static unsigned int small_cache_y_offset;
static unsigned int large_cache_x_offset;
static unsigned int large_cache_y_offset;
static struct map_tile small_map_cache[SMALLCACHESIZE][SMALLCACHESIZE];
static struct map_tile large_map_cache[LARGECACHESIZE][LARGECACHESIZE];

/*
 * Open the map with the given name.
 */
int open_map( const char *filename ) {
    struct map_header header;
    current_map = fopen(filename, "rb");
    if( current_map != NULL ) {
        if( fread(&header, sizeof(struct map_header), 1, current_map) ) {
            map_height = header.height;
            map_width = header.width;
        }
    }
    /* To save us some effort on every call of get_tile(), we have to start by
       filling up the caches. The easiest way to do that is to initialize the
       cache offsets to 1 more than the large cache size and then trying to
       retrieve the tile at (0, 0). */
    small_cache_x_offset = LARGECACHESIZE + 1;
    small_cache_y_offset = LARGECACHESIZE + 1;
    large_cache_x_offset = LARGECACHESIZE + 1;
    large_cache_y_offset = LARGECACHESIZE + 1;
    map_data_offset = sizeof(struct map_header);
    get_tile(0, 0);
    return 0;
}

/*
 * Returns the distance between two given values.
 */
static unsigned int find_delta( unsigned int x1, unsigned int x2 ) {
    return x1 > x2 ? x1 - x2 : x2 - x1;
}

/*
 * Returns the width of the current map.
 */
unsigned int map_get_width() {
    /* TODO: Check that a map is currently open. */
    return map_width;
}

/*
 * Returns the height of the current map.
 */
unsigned int map_get_height() {
    /* TODO: Check that a map is currently open. */
    return map_height;
}

/*
 * Returns true iff the given x/y coordinate lies inside the current map.
 */
int map_in_range( unsigned int x, unsigned int y ) {
    /* TODO: Check that a map is currently open. */
    return x < map_width && y < map_height;
}

/*
 * Returns the tile located at the given x/y location.
 */
struct map_tile get_tile( unsigned int x, unsigned int y ) {
    unsigned int i;
    unsigned int j;
    unsigned int cache_x_delta;
    unsigned int cache_y_delta;
    /* Check small cache for cache hit */
    if( x - small_cache_x_offset < SMALLCACHESIZE &&
        y - small_cache_y_offset < SMALLCACHESIZE ) {
        /* Small cache hit! */
        return small_map_cache[x - small_cache_x_offset][y - small_cache_y_offset];
    }
    else {
        /* Check large cache for cache hit */
        if( x - large_cache_x_offset < LARGECACHESIZE &&
            y - large_cache_y_offset < LARGECACHESIZE ) {
            /* Large cache hit, move small cache */
            if( x > SMALLCACHESIZE / 2 ) {
                if( x + (SMALLCACHESIZE / 2) >= map_width ) {
                    small_cache_x_offset = map_width - SMALLCACHESIZE - 1;
                }
                else {
                    small_cache_x_offset = x - (SMALLCACHESIZE / 2);
                }
            }
            else {
                small_cache_x_offset = 0;
            }
            if( y > SMALLCACHESIZE / 2 ) {
                if( y + (SMALLCACHESIZE / 2) >= map_height ) {
                    small_cache_y_offset = map_height - SMALLCACHESIZE - 1;
                }
                else {
                    small_cache_y_offset = y - (SMALLCACHESIZE / 2);
                }
            }
            else {
                small_cache_y_offset = 0;
            }
            /* Check if we have to move the large cache */
            if( small_cache_x_offset >= large_cache_x_offset &&
                small_cache_x_offset < large_cache_x_offset + LARGECACHESIZE -
                    SMALLCACHESIZE &&
                small_cache_y_offset < large_cache_y_offset + LARGECACHESIZE -
                    SMALLCACHESIZE &&
                small_cache_y_offset >= large_cache_y_offset ) {
                /* Don't have to move the large cache, so reset small cache and
                   return the tile */
                cache_x_delta = small_cache_x_offset - large_cache_x_offset;
                cache_y_delta = small_cache_y_offset - large_cache_y_offset;
                for( i = 0; i < SMALLCACHESIZE; i++ ) {
                    for( j = 0; j < SMALLCACHESIZE; j++ ) {
                        small_map_cache[i][j] =
                            large_map_cache[i + cache_x_delta][j + cache_y_delta];
                    }
                }
                return small_map_cache[x - small_cache_x_offset][y - small_cache_y_offset];
            }
        }
        /* Large cache miss or large cache needs to be moved */
        cache_x_delta = x - (LARGECACHESIZE / 2);
        cache_y_delta = y - (LARGECACHESIZE / 2);
        if( find_delta(cache_x_delta, large_cache_x_offset) < LARGECACHESIZE &&
            find_delta(cache_y_delta, large_cache_y_offset) < LARGECACHESIZE ) {
            /* There's overlap, so let's save us some trouble */
            /* TODO */
        }
        if( x > LARGECACHESIZE / 2 ) {
            if( x + (LARGECACHESIZE / 2) >= map_height ) {
                large_cache_x_offset = map_width - LARGECACHESIZE - 1;
            }
            else {
                large_cache_x_offset = cache_x_delta;
            }
        }
        else {
            large_cache_x_offset = 0;
        }
        if( y > LARGECACHESIZE / 2 ) {
            if( y + (LARGECACHESIZE / 2) >= map_height ) {
                large_cache_y_offset = map_width - LARGECACHESIZE - 1;
            }
            else {
                large_cache_y_offset = cache_y_delta;
            }
        }
        else {
            large_cache_y_offset = 0;
        }
        /* Fetching map data from a file and placing it in our cache is fairly
           straight-forward. Map data is written row by row into the data file,
           so all we have to do is seek to the next line, read in the data, and
           fill in the large cache for every row.
           */
        for( i = 0; i < LARGECACHESIZE; i++ ) {
            fseek( current_map, map_data_offset +
                ( (large_cache_y_offset + i - 1) * map_width +
                large_cache_x_offset ) * sizeof(struct map_tile), SEEK_SET );
            fread( large_map_cache[i], sizeof(struct map_tile), LARGECACHESIZE,
                current_map );
        }
        /* Reset small cache and return the tile */
        cache_x_delta = small_cache_x_offset - large_cache_x_offset;
        cache_y_delta = small_cache_y_offset - large_cache_y_offset;
        for( i = 0; i < SMALLCACHESIZE; i++ ) {
            for( j = 0; j < SMALLCACHESIZE; j++ ) {
                small_map_cache[i][j] =
                    large_map_cache[i + cache_x_delta][j + cache_y_delta];
            }
        }
        return small_map_cache[x - small_cache_x_offset][y - small_cache_y_offset];
    }
}

/*
 * Debugger method. Prints out the contents of both the small and large caches.
 */
void dump_caches() {
    int i;
    int j;
    for( j = 0; i < SMALLCACHESIZE; i++ ) {
        for( i = 0; j < SMALLCACHESIZE; j++ ) {
            fprintf( stdout, "%d", small_map_cache[i][j].type );
        }
        fprintf( stdout, "\n" );
    }
    for( j = 0; i < LARGECACHESIZE; i++ ) {
        for( i = 0; j < LARGECACHESIZE; j++ ) {
            fprintf( stdout, "%d", large_map_cache[i][j].type );
        }
        fprintf( stdout, "\n" );
    }
}
